package com.mic.yzzn.plc.core.internet;

import com.mic.yzzn.plc.common.control.datastructure.PLCDataStatus;
import com.mic.yzzn.plc.common.ins.datastructure.PLCInstructionTuple;
import com.mic.yzzn.plc.common.instructions.basic.PLCRequestInstruction;
import com.mic.yzzn.plc.common.instructions.basic.PLCResponseInstruction;
import com.mic.yzzn.plc.core.ins.toolUtils.ArrayQueue;
import com.mic.yzzn.plc.core.ins.toolUtils.ExceptionToString;
import com.mic.yzzn.plc.core.plcevents.PLCFeedbackEvent;
import com.mic.yzzn.plc.core.plcevents.listener.PLCFeedbackEventListener;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import purejavacomm.*;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.TooManyListenersException;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Created by ASH on 16/7/17.
 *
 * 用于与 COM 口连接的 PLC 进行通信的类
 */
public class COMInstructionDispatcher implements SerialPortEventListener, InstructionDispatcher {
    /** 波特率 */
    private int baudRate;
    /** 数据位数 */
    private int dataBits;
    /** 停止位 */
    private int stopBit;
    /** 奇偶校验 */
    private int parity;
    /** 串口名称 */
    private String name;
    /** 数据输入流 */
    private InputStream in;
    /** 数据输出流 */
    private DataOutputStream out;
    /** 串口对象 */
    private CommPort commPort;
    /** 反馈事件监听者 */
    private CopyOnWriteArrayList<PLCFeedbackEventListener> listeners = new CopyOnWriteArrayList<>();
    /** 指令发送的队列 */
    private com.mic.yzzn.plc.core.ins.toolUtils.ArrayQueue<PLCRequestInstruction> requestInstructions = new ArrayQueue<>();

    private static final Logger logger = LogManager.getLogger(COMInstructionDispatcher.class);


    public void disconnect() {
        SerialPort serialPort = (SerialPort) commPort;
        serialPort.removeEventListener();
        serialPort.notifyOnDataAvailable(false);
        serialPort.notifyOnBreakInterrupt(false);
        serialPort.notifyOnCarrierDetect(false);
        serialPort.notifyOnParityError(false);
        serialPort.notifyOnCTS(false);
        serialPort.notifyOnDSR(false);
        serialPort.notifyOnRingIndicator(false);
        serialPort.notifyOnOutputEmpty(false);
        serialPort.notifyOnFramingError(false);

        commPort.close();
    }

    public void connect() throws IOException, NoSuchPortException, PortInUseException, UnsupportedCommOperationException, TooManyListenersException {

        CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(name);
        if ( portIdentifier.isCurrentlyOwned() )
        {
            throw new IOException("Error: Port is currently in use");
        } else {

            commPort = portIdentifier.open(this.getClass().getName(),2000);

            if ( commPort instanceof SerialPort ) {
                // 设置连接参数
                SerialPort serialPort = (SerialPort) commPort;
                serialPort.setSerialPortParams(baudRate, dataBits, stopBit, parity);

                // 初始化输入输出流
                this.in = serialPort.getInputStream();
                this.out = new DataOutputStream(serialPort.getOutputStream());

                // 添加监听线程
                serialPort.addEventListener(this);
                serialPort.notifyOnDataAvailable(true);
                serialPort.notifyOnBreakInterrupt(true);
                serialPort.notifyOnCarrierDetect(true);
                serialPort.notifyOnParityError(true);
                serialPort.notifyOnCTS(true);
                serialPort.notifyOnDSR(true);
                serialPort.notifyOnRingIndicator(true);
                serialPort.notifyOnOutputEmpty(true);
                serialPort.notifyOnFramingError(true);
            } else {
                throw new IllegalArgumentException("Error: Only serial ports are handled by this example.");
            }
        }
    }

     /**
     * 构造函数
     * @param baudRate 波特率
     * @param dataBits 数据位数
     * @param stopBit 停止位
     * @param parity 奇偶校验
     * @param name COM口名
     * @throws Exception
     */
    public COMInstructionDispatcher(int baudRate, int dataBits, int stopBit, int parity, String name)
            throws IOException, NoSuchPortException, PortInUseException, UnsupportedCommOperationException, TooManyListenersException {
        this.baudRate = baudRate;
        this.dataBits = dataBits;
        this.stopBit = stopBit;
        this.parity = parity;
        this.name = name;
        connect();
    }

    private String readLine(InputStream in) throws IOException {
        int data;
        byte[] buffer = new byte[128];
        int len = 0;
        while ((data = in.read()) > -1) {
            if (data == '\r') {
                break;
            }
            buffer[len++] = (byte) data;
        }

        return new String(buffer,0,len);
    }

    /**
     * COM口数据到达时间监听
     * @param serialPortEvent COM口事件
     */
    @Override
    public void serialEvent(SerialPortEvent serialPortEvent) {
        if (serialPortEvent.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
            try {
                String response = readLine(in);
                logger.info("COM Response: " + response);
                notifyListener(response, PLCDataStatus.PLC_DATA_STATUS_OK);

            } catch ( IOException e ) {
                logger.error(e, e);
                try {
                    disconnect();
                    connect();
                } catch (IOException e1) {
                    logger.error(e1, e1);
                } catch (NoSuchPortException e1) {
                    logger.error(e1, e1);
                } catch (PortInUseException e1) {
                    logger.error(e1, e1);
                } catch (UnsupportedCommOperationException e1) {
                    logger.error(e1, e1);
                } catch (TooManyListenersException e1) {
                    logger.error(e1, e1);
                }
                notifyListener("%01!43**", PLCDataStatus.PLC_DATA_STATUS_COM_READ_ERROR);
            }
        } else {
            logger.warn("EventType: " + serialPortEvent.getEventType());
        }
    }

    /**
     * 通知监听者响应已经准备好
     * @param response 响应指令
     * @param state 响应状态
     */
    private void notifyListener(String response, int state) {
        logger.debug("[ready to notify]");
        PLCInstructionTuple tuple = new PLCInstructionTuple();
        tuple.requestInstruction = requestInstructions.remove();
        tuple.responseState = state;
        tuple.responseString = response;
        for (PLCFeedbackEventListener listener : listeners) {
            logger.debug("[notify listener with " + response + "]");
            listener.notifyFeedbackEvent(new PLCFeedbackEvent(this, tuple));
        }
    }

    /**
     * 添加数据反馈监听者
     * @param listener 监听者
     */
    public void addListener(PLCFeedbackEventListener listener) {
        listeners.add(listener);
    }

    /**
     * 移除数据反馈监听者
     * @param listener 监听者
     */
    public void removeListener(PLCFeedbackEventListener listener) {
        listeners.remove(listener);
    }

    /**
     * 向COM口写指令
     * @param ins 指令字符串
     */
    private void send(String ins) {
        try
        {
            out.writeUTF(ins);
            out.flush();
        } catch ( IOException e ) {
            logger.error(e, e);
            try {
                disconnect();
                connect();
            } catch (IOException e1) {
                logger.error(e1, e1);
            } catch (NoSuchPortException e1) {
                logger.error(e1, e1);
            } catch (PortInUseException e1) {
                logger.error(e1, e1);
            } catch (UnsupportedCommOperationException e1) {
                logger.error(e1, e1);
            } catch (TooManyListenersException e1) {
                logger.error(e1, e1);
            }
            notifyListener("%01!43**", PLCDataStatus.PLC_DATA_STATUS_COM_WRITE_ERROR);
        }
    }

    /**
     * 发送请求指令
     * @param instruction 请求指令
     */
    public void sendInstruction(PLCRequestInstruction instruction) {
        logger.info("COM Write: " + instruction);
        requestInstructions.add(instruction);
        send(instruction.toString());
    }
}
